<?php

    /**
     * Main controller
     *
     * @license      GNU Lesser General Public Licence see LICENCE-LGPL file or http://www.gnu.org/licenses/lgpl.html
     */
	final class Main {

	////////////////////////////////////////////////////////////////////////////
	//                                Variables                               //
	////////////////////////////////////////////////////////////////////////////

		/**
		 * Singleton Zend_Registry object
		 *
		 * @var Zend_Registry
		 */
		private static $_registry = null;

		/**
		 * Application
		 *
		 * @var Core_Controller_Coordinator
		 */
		private static $_coordinator = null;
    
		/**
		 * Refresh models
		 *
		 * @var boolean
		 */
		private static $_refreshModels = false;
    
		/**
		 * Memcache
		 *
		 * @var Memcache
		 */
		private static $_memcache = null;
		
        /**
         * loadedModels
         *
         * @var array
         */
		private static $_loadedModels = array();

	////////////////////////////////////////////////////////////////////////////
	//                             Static Methods                             //
	////////////////////////////////////////////////////////////////////////////

		/**
		 * Returns the current version of the application
		 *
		 * @return      string      Current version of the program
		 */
		public static function getVersion()
		{
			return '1.0';
		}

		/**
		 * Main entry point
		 *
		 * @param       string      $end : front / admin end
		 * @param       string      $type : type of request (i.e. json, http, etc.)
         * @param       boolean     $generate : generates models ?
         * @param       boolean     $data : import data ?
		 * @return      void
		 */
		public static function run($end = 'front', $type = 'http', $generate = false, $data = false)
		{
		    self::$_refreshModels = $generate;
            self::_initialize($end, $data);

            /*
             * Coordinator process
             */
            if ($type !== null) {
                self::$_coordinator = Core_Controller_Coordinator::getInstance();

                $requestClass = 'Core_Controller_Request_' . ucfirst(strtolower($type));
                self::$_coordinator->process(new $requestClass());
            }
		}

		/**
		 * Initialization
		 *
		 * @param       string      $end : front / admin end
		 * @param       string      $type : type of request (i.e. json, http, etc.)
		 * @return      void
		 */
		public static function _initialize($end, $data = false)
		{
		    self::setIncludes();
            Core_Meter::getInstance()->start();

            /*
             * Extensions
             */
            self::_loadRequiredExtensions();
            self::_loadMbString();

            /*
             * Registry
             */
            self::_initMemcache();
            self::_initializeRegistry($end);
            Core_Meter::getInstance()->markInterval('Registry loaded');

            /*
             * Database and modules
             */
            self::_initializeDatabaseAdapter();
            Core_Meter::getInstance()->markInterval('Database initialized');

            if (self::$_refreshModels === true) {
                self::_installModules($data);
                Core_Meter::getInstance()->markInterval('Modules initialized');
            }
		}

		/**
		 * Set the include paths
		 *
		 * @return      void
		 */
		public static function setIncludes()
		{
            /*
             *  Serum
             */
            define('SERUM_PATH',                dirname(__FILE__) . DIRECTORY_SEPARATOR);
            define('SERUM_INSTALL_PATH',        realpath(SERUM_PATH         .'../install/')         . DIRECTORY_SEPARATOR);
            define('SERUM_LIB_PATH',            realpath(SERUM_PATH         .'../lib/')             . DIRECTORY_SEPARATOR);

            define('SERUM_MODULE_PATH',         realpath(SERUM_PATH         .'./modules/')              . DIRECTORY_SEPARATOR);
            define('SERUM_PLUGIN_PATH',         realpath(SERUM_PATH         .'./plugins/')              . DIRECTORY_SEPARATOR);

            define('SERUM_VAR_PATH',            realpath(SERUM_PATH         .'./var/')                  . DIRECTORY_SEPARATOR);
            define('SERUM_CONFIG_PATH',         realpath(SERUM_VAR_PATH     .'./config/')               . DIRECTORY_SEPARATOR);

            define('SERUM_UPDATE_PATH',         realpath(SERUM_PATH         .'../updates/migrations/')  . DIRECTORY_SEPARATOR);
            define('SERUM_WWW_PATH',            realpath(SERUM_PATH         .'../')                     . DIRECTORY_SEPARATOR);
            define('WWW_PATH',                  realpath(SERUM_WWW_PATH     .'../')                     . DIRECTORY_SEPARATOR);

            require_once SERUM_LIB_PATH . 'Functions' . DIRECTORY_SEPARATOR . 'Core.php';

            set_include_path(
			    get_include_path()
			    . PATH_SEPARATOR . SERUM_LIB_PATH
			    . PATH_SEPARATOR . SERUM_MODULE_PATH
			    . PATH_SEPARATOR . realpath(SERUM_LIB_PATH .'Dwoo/') . DIRECTORY_SEPARATOR
			);

            /*
             *  Project
             */
            define('PROJECT_WWW_PATH',          Core_IO_Path::combine(WWW_PATH, 'project'));
            define('PROJECT_PATH',              Core_IO_Path::combine(PROJECT_WWW_PATH, 'app'));
            define('PROJECT_INSTALL_PATH',      Core_IO_Path::combine(PROJECT_WWW_PATH, 'install'));
            define('PROJECT_LIB_PATH',          Core_IO_Path::combine(PROJECT_WWW_PATH, 'lib'));
            define('PROJECT_MEDIA_PATH',        Core_IO_Path::combine(PROJECT_WWW_PATH, 'media'));
            define('PROJECT_TEMP_PATH',         Core_IO_Path::combine(PROJECT_WWW_PATH, 'temp'));
            define('PROJECT_TEMP_COMPILED_PATH',Core_IO_Path::combine(PROJECT_TEMP_PATH, 'compiled'));
            define('PROJECT_TEMP_URLS',         Core_IO_Path::combine(PROJECT_TEMP_COMPILED_PATH, 'urls'));
            define('PROJECT_TEMP_LOCALES',      Core_IO_Path::combine(PROJECT_TEMP_COMPILED_PATH, 'locales'));
            define('PROJECT_TEMP_TEMPLATES',    Core_IO_Path::combine(PROJECT_TEMP_COMPILED_PATH, 'templates'));

            define('PROJECT_MODULE_PATH',       Core_IO_Path::combine(PROJECT_PATH, 'modules'));
            define('PROJECT_PLUGIN_PATH',       Core_IO_Path::combine(PROJECT_PATH, 'plugins'));

            define('PROJECT_VAR_PATH',          Core_IO_Path::combine(PROJECT_PATH, 'var'));
            define('PROJECT_LOG_PATH',          Core_IO_Path::combine(PROJECT_VAR_PATH, 'log'));
            define('PROJECT_CONFIG_PATH',       Core_IO_Path::combine(PROJECT_VAR_PATH, 'config'));
            define('PROJECT_BACKUP_PATH',       Core_IO_Path::combine(PROJECT_VAR_PATH, 'backup'));

            define('PROJECT_UPDATE_ROOT_PATH',  Core_IO_Path::combine(PROJECT_WWW_PATH, 'updates'));
            define('PROJECT_UPDATE_PATH',       Core_IO_Path::combine(PROJECT_UPDATE_ROOT_PATH, 'migrations'));

            set_include_path(
			    get_include_path()
			    . PATH_SEPARATOR . PROJECT_LIB_PATH
                . PATH_SEPARATOR . PROJECT_MODULE_PATH
			);
		}

	////////////////////////////////////////////////////////////////////////////
	//                         Private Static Methods                         //
	////////////////////////////////////////////////////////////////////////////

        /**
         * Initializes memcache support
         *
         * @return      void
         */
		private static function _initMemcache() 
        {
            if (self::$_memcache === null && extension_loaded('Memcache')) {
                self::$_memcache = new Memcache();
            }

            if (self::$_memcache !== null) {
                self::$_memcache->connect("127.0.0.1", 11211) or die ("Connexion impossible");
                Core_Meter::getInstance()->markInterval('Memcache loaded');
            }
		}
		
		/**
		 * Loads the registry
		 *
		 * @param       string      $end : front / admin end
		 * @return      void
		 */
		private static function _initializeRegistry($end)
		{
            self::$_registry = Zend_Registry::getInstance();

            if (self::$_memcache !== null) {
                $_registry = self::$_memcache->get(self::_memcachePrefix('_registry'));
            }

            if (self::$_memcache !== null && $_registry['valid'] === true && self::$_refreshModels === false) {
                /*
                 * Retrieves registry from memcache
                 */
                foreach ($_registry['registry'] as $key => $value) {
                    self::$_registry[$key]  = $value;
                }

            } else {
                /*
                 * Builds registry
                 */
                self::$_registry['modules']         = array();
                self::$_registry['orderedModules']  = array();

                // Modules list (Serum + Project)
                foreach (array(SERUM_MODULE_PATH, PROJECT_MODULE_PATH) as $currentPath) {
                    $modules = Core_IO_Directory::getDirectories($currentPath, false);

                    foreach ($modules as $module) {
                        if ($module == '.svn') {
                            continue;
                        }

                        self::$_registry['modules'][$module] = Core_IO_Path::combine($currentPath, $module);
                    }
                }

                self::$_registry['end'] 	= $end;
                self::$_registry['section'] = 'staging';

                try {
                    self::$_registry['config'] = new Core_Config('global.ini.php', self::$_registry['section']);

                } catch (Exception $e) {
                    // If application global.ini.php don't exists, redirect to installer
                    header("Location: serum/install/");
                }

                // Plugins list
                self::$_registry['plugins'] = array();

                foreach (array(SERUM_PLUGIN_PATH, PROJECT_PLUGIN_PATH) as $currentPath) {
                    $plugins = Core_IO_Directory::getDirectories($currentPath, false);

                    foreach ($plugins as $plugin) {
                        if ($plugin == '.svn') {
                            continue;
                        }

                        self::$_registry['plugins'][$plugin] = Core_IO_Path::combine($currentPath, $plugin);
                    }
                }

                // Gets modules configuration
                self::$_registry['modules_config'] = Core_Module_Config::getInstance('config.ini.php', self::$_registry['section']);

                if (self::$_memcache !== null) {
                    $arrayTmp  = array();
                    foreach (self::$_registry as $key => $value) {
                        $arrayTmp[$key] = $value;
                    }

                    $_registry = array('valid' => true,
                                       'registry' => $arrayTmp);

                    self::$_memcache->set(self::_memcachePrefix('_registry'), $_registry);
                }
            }

            if (self::$_memcache !== null && self::$_registry['end'] != $end) {
                self::$_registry['end'] = $end;
            }
		}

        /**
		 * Loads module and its dependencies
         *
         * Generates Doctrine models in /modules/<module>/Db for each installed module
		 *
		 * @param       array      $module : name, path, requires
		 * @param       boolean    $refreshModels
		 * @return      void
		 */
        private static function _loadModule($module) {
            if (array_key_exists($module['name'], self::$_registry['loadedModules']) && self::$_registry['loadedModules'][$module['name']]['loaded'] === true) {
                return;
            }
            
            if (self::$_refreshModels && isset($module['requires'])) {
                foreach($module['requires'] as $require) {
                    self::_loadModule(self::$_registry['loadedModules'][$require]);
                }
            }

            self::$_registry['loadedModules'][$module['name']]['loaded']    = true;
            self::$_registry['orderedModules'][]                            = $module['name'];

            // Module is loaded
            $modelFile = Core_IO_Path::combine($module['path'], 'models.yml');
            $modelPath = Core_IO_Path::combine($module['path'], 'Classes', 'Db');

            if (!Core_IO_Directory::exists($modelPath)) {
                return;
            }
            
            // Generate API database classes
            if (self::$_refreshModels === true) {
		            $apiModelPath = Core_IO_Path::combine($module['path'], 'models');
		
		            if (Core_IO_Directory::exists($apiModelPath)) {
		                $files = Core_IO_Directory::getFiles($apiModelPath, false);
		
		                foreach ($files as $file) {
		                    if (Core_IO_Path::getExtension($file) == '.yml') {
		                        $apipath = Core_IO_Path::combine($apiModelPath, $file);
		
		                        // Generate and load module's model only if module contains a YAML file
		                        if (@is_readable($apipath)) {
		                            try {
                                        Doctrine::generateModelsFromYaml($apipath, $modelPath);
		                            } catch (Exception $e) {
		                                throw new Core_Exception('Fatal error: unable to compile API models');
		                            }
		                        }
		                    }
		                }
		            }
            }

            // Generate and load module's model only if module contains a models.yml
            if (Core_IO_File::isReadable($modelFile)) {
	            if (self::$_refreshModels == true) {
                    try {
                        Doctrine::generateModelsFromYaml($modelFile, $modelPath);
                    } catch (Exception $e) {
                        throw new Core_Exception("Fatal error: unable to compile YAML file '". $modelFile ."' : ". $e->getMessage());
                    }
                }

                try {
                    $result = Doctrine::loadModels($modelPath, Doctrine::MODEL_LOADING_CONSERVATIVE);
                    Doctrine_Manager::getInstance()->setAttribute('use_dql_callbacks', true);
                    
                    self::$_loadedModels = array_merge(self::$_loadedModels, $result);

                } catch (Exception $e) {
                    throw new Core_Exception("Fatal error: unable to load database model files '". $modelPath ."'");
                }
            }
        }

        /**
		 * Returns the database resource
		 *
         * @param       boolean    $refreshModels
		 * @throws		Core_Exception
		 * @return		void
		 */
		private static function _initializeDatabaseAdapter()
        {
            if (self::$_registry === null) {
                throw new Core_Exception('Registry is a null reference');
            }

            // Initializes temporary array
            self::$_registry['loadedModules'] = array();

            // Gets modules configuration
            //$modulesConfiguration = Core_Module_Config::getInstance('config.ini.php', self::$_registry['section']);

            // Check modules dependencies
            foreach (self::$_registry['modules'] as $module => $path) {
                self::$_registry['config']->{$module}       = self::$_registry['modules_config']->{strtolower($module)};
                self::$_registry['loadedModules'][$module]  = array('name'      => $module,
                                                                    'path'      => $path,
                                                                    'requires'  => array(),
                                                                    'loaded'    => false);

                if (!empty(self::$_registry['config']->{$module}->requires)) {
                    // Module has dependencies
                    $required = explode(',', self::$_registry['config']->{$module}->requires);
                    foreach ($required as $require) {
                        self::$_registry['loadedModules'][$module]['requires'][] = trim($require);
                    }
                }
            }
            
            if (self::$_memcache !== null) {
                $_loadedModels = self::$_memcache->get(self::_memcachePrefix('_loadedModels'));
            }
                
            if (self::$_memcache !== null && $_loadedModels['valid'] === true && self::$_refreshModels === false) {
                foreach ($_loadedModels['modelsList'] as $className => $path) {
                    Doctrine::loadModel($className, $path);
                }

            } else {
                self::$_loadedModels = array();
            	  
                foreach (self::$_registry['loadedModules'] as $module) {
                    self::_loadModule($module);
                }

                if (self::$_memcache !== null) {
                    $arrayTmp = array('valid' => true,
                                      'modelsList'  => self::$_loadedModels);
                    self::$_memcache->set(self::_memcachePrefix('_loadedModels'), $arrayTmp);
                }
            }

            // Unsets temporary array
            unset(self::$_registry['loadedModules']);

            // Models are loaded, make connection to the database
            $strConn  = self::$_registry['config']->database->adapter
                        . '://'
                        . self::$_registry['config']->database->params->username;
              
            if (!empty(self::$_registry['config']->database->params->password)) {
                $strConn .= ':' . self::$_registry['config']->database->params->password;
            }

            $strConn .= '@'
                        . self::$_registry['config']->database->params->host
                        . '/'
                        . self::$_registry['config']->database->params->dbname;

            try {
                $conn     = Doctrine_Manager::connection($strConn);
                $profiler = new Doctrine_Connection_Profiler();

                $conn->setListener($profiler);
                Core_Meter::getInstance()->setSqlProfiler($profiler);

            } catch (Exception $e) {
                throw new Core_Exception('Unable to load connect to the database');
            }
        }

		/**
		 * Dynamically install the modules if there aren't installed
		 *
		 * @throws      Core_Exception
         * @param       boolean         $data
		 * @return      void
		 */
		private static function _installModules($data)
		{
            if ($data) {
                // Generates table
                Doctrine::createTablesFromModels();
            }

            $installers         = array();
            $installedModules   = Doctrine_Query::create()
                                    ->select('m.slug')
                                    ->from('DbModule m INDEXBY m.slug')
                                    ->execute(array(), Doctrine::HYDRATE_ARRAY);

            // Check modules dependencies
            foreach (self::$_registry['orderedModules'] as $module) {
                $installerPath  = Core_IO_Path::combine(self::$_registry['modules'][$module], 'Setup', 'Installer.php');

                if (Core_IO_File::exists($installerPath)) {
                    require_once $installerPath;

                    $className  = $module .'_Setup_Installer';
                    $installer  = new $className();

                    $installer->preInstall();

                    // Launch setup if module isn't registered.
                    if (!array_key_exists(strtolower($module), $installedModules)) {
                        $installers[$module] = $installer;
                        $installers[$module]->run();
                    }
                }
            }

            if ($data) {
                /*
                 * Import fixtures
                 */
                foreach (array(Core_IO_Path::combine(SERUM_INSTALL_PATH, 'fixtures.yml'), Core_IO_Path::combine(PROJECT_INSTALL_PATH, 'fixtures.yml')) as $fixtures) {
                    if (Core_IO_File::exists($fixtures)) {
                        $fixturesCache = Core_IO_File::readAll($fixtures);

                        if (strlen($fixturesCache) > 0) {
                            // Create temporary file for fixtures compilation
                            $tempFixture = Core_IO_Path::combine(PROJECT_TEMP_PATH, 'fixtures.tmp.yml');
                            Core_IO_File::writeAll($tempFixture, $fixturesCache);

                            // Import compiled fixtures and delete cache
                            $data = new Doctrine_Data();
                            $data->importData($tempFixture);

                            // Delete fixtures cache
                            Core_IO_File::delete($tempFixture);
                        }
                    }
                }
            }

            // Execute post fixtures operations
            foreach (self::$_registry['orderedModules'] as $module) {
                $path = self::$_registry['modules'][$module];

                if (array_key_exists($module, $installers)) {
                    $installers[$module]->postInstall();
                }
            }
        }

		/**
		 * Tries to load the mbstring extension required by the application
		 *
		 * @return      boolean
		 */
		private static function _loadMbString()
		{
			$result  = self::_loadExtension('mbstring');

			$result &= @ini_set('mbstring.language', 'neutral');
			$result &= @ini_set('mbstring.internal_encoding', 'UTF-8');
			$result &= @ini_set('mbstring.func_overload', '7');

			return $result;
		}

		/**
		 * Tries to load all the extensions required by the application
		 *
		 * @return      boolean
		 */
		private static function _loadRequiredExtensions()
		{
			$result = true;
			foreach (array('simplexml', 'pdo_mysql', 'soap') as $ext) {
				if (!self::_loadExtension($ext)) {
					$result = false;
				}
			}

			return $result;
		}

		/**
		 * Tries to dynamically load an extension if not loaded
		 *
		 * @param       string      $ext : the extension to load
		 * @return      boolean
		 */
		private static function _loadExtension($ext)
		{
			if (@extension_loaded($ext)) {
				return true;
			}

			if (@ini_get('enable_dl') !== 1 || @ini_get('safe_mode') === 1) {
				return false;
			}

			$file = (PHP_SHLIB_SUFFIX === 'dll' ? 'php_' : '') . $ext . '.' . PHP_SHLIB_SUFFIX;
			return @dl($file);
		}

        /**
         * Returns varname for specific path
         *
         * @param       string      $var
         * @return      string
         */
        private static function _memcachePrefix($var)
        {
            return '_'. str_replace(DIRECTORY_SEPARATOR, '_', PROJECT_PATH). $var;
        }
	}